<?php
class Event
{
	/**
	 * Broadcast the message to all listeners. (default)
	 */
	const MODE_BROADCAST = 0;
	
	/**
	 * Withhold messages until queue is flushed.
	 * 
	 * To process messages call Event::processHeld($tag).
	 */
	const MODE_HOLD = 1;
	
	/**
	 * Withhold messages until the first listener is registered.
	 */
	const MODE_PICKUP = 2;
	
	/**
	 * Worst kind of log message, we really don't want any of these.
	 */
	const LOG_ERROR = 3;
	
	/**
	 * For non-critical, strange occurrances that warrant some investigating.
	 */
	const LOG_WARN = 2;
	
	/**
	 * For general information.
	 */
	const LOG_INFO = 1;
	
	/**
	 * For temporary or especially verbose debug information.
	 */
	const LOG_DEBUG = 0;
	
	/**
	 * Contains the microtime when the Event broker was initialized.
	 */
	static protected $startup;
	
	/**
	 * Dispatch an event.
	 *
	 * @param string $tag Tag for the event to be triggered.
	 * @param array $data Any information you want to pass on along with the event.
	 */
	static public function dispatch($tag, $data = array())
	{
		$tag = self::getTag($tag);
		$mode = $tag->getMode();

		switch($mode) {
			case self::MODE_HOLD:
				$tag->addHeld($data);
				break;
			case self::MODE_BROADCAST:
			default:
				foreach ($tag->getListeners() as $listener) {
					call_user_func($listener, $data);
				}
		}
	}
	
	/**
	 * Add an event listener.
	 * 
	 * @param string $tag
	 * @param callback $callback
	 */
	static public function addListener($tag, $callback)
	{
		$tag = self::getTag($tag);
		
		$tag->addListener($callback);
		
		// If mode is MODE_PICKUP, flush now
		if ($tag->isMode(self::MODE_PICKUP)) {
			$tag->setMode(self::MODE_BROADCAST);
			$tag->processHeld();
		}
	}
	
	/**
	 * Sets some safe defaults to preserve early debug message until the logging
	 * system is ready.
	 *
	 * You can run the framework with these default settings, but it is strongly
	 * recommend to later add proper event handlers and execute
	 * deactivateStartupMode().
	 * 
	 * This function is also (ab)used to do some other startup work.
	 */
	static public function activateStartupMode()
	{
		// Make sure log messages are kept until retrieved by a log handler
		self::setMode('log', self::MODE_HOLD);
		
		// Initialize Event class
		self::$startup = microtime(true);
		register_shutdown_function(array('Event', 'onShutdown'));
		set_error_handler(array('Event', 'phpError'));
		
		// Finally, log our initialization
		self::log('framework.event', 'Event broker online');
	}
	
	/**
	 * Deactives startup mode and releases held debug messages.
	 */
	static public function deactivateStartupMode()
	{
		self::processHeld('log');
	}
	
	/**
	 * This is the placeholder error handler that is used until a better one is
	 * loaded.
	 *
	 * @param array $data
	 */
	static public function handleFatal($data)
	{
		die('<strong>CMS Error:</strong> '.$data['message']);
	}
	
	/**
	 * Shorthand function for dispatching log events.
	 *
	 * @param string $component Component that raised the log entry
	 * @param string $level Log level
	 * @param string $message Message text.
	 * @param array $data Optional additional data.
	 */
	static public function log($component, $message, $level = self::LOG_INFO, $data = array())
	{
		$data['timestamp'] = microtime(true)-self::$startup;
		$data['component'] = $component;
		$data['level'] = $level;
		$data['message'] = $message;
		
		self::dispatch('log', $data);
		
		if ($level == Event::LOG_ERROR) exit;
	}
	
	/**
	 * Release events that were held by MODE_HOLD functionality.
	 * 
	 * If MODE_HELD is still set when this function is called, the mode is reverted
	 * back to the default MODE_BROADCAST.
	 */
	static public function processHeld($tag)
	{
		self::getTag($tag)->processHeld();
	}
	
	static public function setMode($tag, $mode = self::MODE_BROADCAST)
	{
		self::getTag($tag)->setMode($mode);
	}
	
	/**
	 * Handles the shutdown event.
	 */
	static public function onShutdown()
	{
		Event::log('framework.event', 'Shutdown initiated');
		self::dispatch('shutdown');
	}
	
	static private $tags = array();
	static protected function getTag($tag)
	{
		if (!isset(self::$tags[$tag])) {
			self::$tags[$tag] = new EventTag($tag);
		}
		
		return self::$tags[$tag];
	}
	
	/**
	 * Custom error handler.
	 * 
	 * Will enter PHP error into the logs as well as handle more severe errors where possible.
	 *
	 * @param int $errno The level of error raised as per PHP's error level constants.
	 * @param string $errstr The error message.
	 * @param string $errfile The file where the error occurred.
	 * @param int $errline The line where the error occurred.
	 * @param array $errcontext The variables in the context where the error occurred.
	 */
	static public function phpError($errno, $errstr, $errfile = null, $errline = null, $errcontext = array())
	{
		$mapping = array(
			E_WARNING => self::LOG_WARN,
			E_NOTICE => self::LOG_INFO,
			E_USER_ERROR => self::LOG_ERROR,
			E_USER_WARNING => self::LOG_WARN,
			E_USER_NOTICE => self::LOG_INFO,
			E_STRICT => self::LOG_DEBUG,
			E_RECOVERABLE_ERROR => self::LOG_ERROR
		);
		
		if (error_reporting() === 0) {
			// Error was triggered within a @-prepended line
		}
		
		$file = '';
		if ($errfile !== null) {
			// If file is within framework ...
			$frameworkPath = Config::get('path');
			if (substr($errfile, 0, strlen($frameworkPath)) == $frameworkPath) {
				// ... make path relative to framework root
				$errfile = substr($errfile, strlen($frameworkPath));
			}
			
			$file = ' in '.$errfile;
		}
		
		if ($errline !== null) {
			$file .= '['.$errline.']';
		}
		
		Event::log('php', $errstr.$file, $mapping[$errno], $errcontext);
		
		return true;
	}
}

class EventTag
{
	/**
	 * Tag name
	 *
	 * @var string
	 */
	protected $tag;
	
	/**
	 * Current mode for this tag.
	 */
	protected $mode = Event::MODE_BROADCAST;
	
	/**
	 * Cache for held events.
	 */
	protected $held = array();
	
	/**
	 * List of event listeners.
	 */
	protected $listeners = array();
	
	public function __construct($tag)
	{
		$this->tag = $tag;
	}
	
	public function isMode($mode)
	{
		return $this->mode == $mode; 
	}
	
	public function setMode($mode)
	{
		$this->mode = $mode;
	}
	
	public function getMode()
	{
		return $this->mode;
	}
	
	public function addListener($listener)
	{
		$this->listeners[] = $listener;
	}
	
	public function getListeners()
	{
		return $this->listeners;
	}
	
	public function processHeld()
	{
		if ($this->isMode(Event::MODE_HOLD)) $this->setMode(Event::MODE_BROADCAST);
		
		foreach ($this->held as $data) {
			Event::dispatch($this->tag, $data);
		}
	}
	
	public function addHeld($data)
	{
		$this->held[] = $data;
	}
}
?>